08 原型、构造函数与类
> Last Format Time:8/11/2026 21:45:39
本笔记由两个 JavaScript 教程目录中的同主题内容合并而成;全部旧知识保留在“旧笔记知识全集(按来源保留)”中,并由导航逐项索引。
当前前端开发关键要点
> [!important] 学习优先级:P1 > - 理解实例、构造函数、prototype 与原型链的关系,能读懂第三方库和面试中的继承代码。 > - 业务代码优先使用 class 表达对象模型,但要知道它仍建立在原型机制之上。 > - 组合通常比深层继承更易维护;Mixin、私有字段和静态成员应按清晰边界使用。 > - 掌握 new 的执行过程以及 instanceof 的原型链判断逻辑。
知识点导航
[现代JS教程与阮一峰ES6 · 2函数/构造函数与New](./08 原型、构造函数与类.md)
- 构造函数
- 手动实现一个new
[现代JS教程与阮一峰ES6 · 4原型与类/原型](./08 原型、构造函数与类.md)
- 构造函数的Prototype
- 原生的原型
- 原型方法和没有__proto__的对象
[现代JS教程与阮一峰ES6 · 4原型与类/原型链](./08 原型、构造函数与类.md)
- 原型链
[现代JS教程与阮一峰ES6 · 4原型与类/类](./08 原型、构造函数与类.md)
- 基本语法
- 类表达式
- Getters/setters
- 计算属性名称
- “类字段”
- 类继承
- 重写方法
- 静态属性和静态方法
- 受保护的、私有的属性和方法
- 私有的属性
- 扩展内建类(使用内建类做父类)
- 类检查
- Mixin模式
- 🤔 为什么需要 Mixin 模式?
- 🛠️ 如何实现 Mixin 模式?
- 使用
Object.assign()进行属性复制 - 使用高阶函数(类工厂)
- ✨ Mixin 模式的优缺点
- 优点
- 缺点
- 🌐 实际应用:Vue.js 中的 Mixin
[廖雪峰教程 · 6对象/6.0 简述](./08 原型、构造函数与类.md)
- 正文主题:6.0 简述
[廖雪峰教程 · 6对象/6.1 创建对象](./08 原型、构造函数与类.md)
- 创建对象
- 构造函数
- 忘记写new怎么办
[廖雪峰教程 · 6对象/6.2 原型继承](./08 原型、构造函数与类.md)
- 原型继承
[廖雪峰教程 · 6对象/6.3 类的继承](./08 原型、构造函数与类.md)
- 类的继承
- class继承
现代前端补充与纠错
> [!info] 修改标记 > - 【修正】:旧教程中错误、过时或容易误导的内容。 > - 【补充】:旧教程未覆盖、但当前前端开发需要掌握的内容。 > - 【修正代码】:替换或校正了旧代码示例。
> [!warning] 下方保留教程原文;class 是基于原型机制的语法体系,但不能把所有 class 行为机械等同于旧式构造函数写法。
原型与 class
- 【补充】
new会创建对象、连接原型、以该对象调用构造器,并按构造器返回值规则决定最终结果。 - 【补充】 class 声明不会像函数声明那样完整提升,主体默认严格模式,方法不可直接作为构造器调用。
- 【补充】 公有字段是实例自身属性;
#private私有字段由语言强制约束,不等同于命名约定_private。 - 【补充】 静态字段和静态块适合类级初始化,但要注意模块求值时的副作用和测试隔离。
设计建议
- 【补充】 继承适合稳定的“是一个”关系;前端组件和业务模型通常优先组合、委托与小型纯函数。
- 【补充】 修改内建原型会影响全局环境并可能与未来标准冲突,除受控 polyfill 外应避免。
- 【修正】 跨 realm 判断不要盲信
instanceof;数组优先Array.isArray,其他对象结合领域 API 判断。
旧笔记知识全集(按来源保留)
现代JS教程与阮一峰ES6 · 2函数/构造函数与New
构造函数
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以==大写字母==开头。
- 它们==只能由
"new"== 操作符来执行。
当一个函数被使用 new 操作符执行时,它按照以下步骤:
- 一个==新的空对象==被创建并==绑定给
this==。(如果是内置的对象还要链接到原型链) - 函数体执行。通常它会修改
this,为其添加新的属性。 - 返回
this的值。构造函数没有return语句。
但是,如果这有一个 return 语句,那么规则就简单了:
- 如果
return返回的是一个对象,则返回这个对象,而不是this。 - 如果
return返回的是一个原始类型,则忽略。
从技术上讲,任何函数(除了箭头函数,它没有自己的 this)都可以用作构造器。即可以通过 new 来运行,它会执行上面的算法。“首字母大写”是一个共同的约定,以明确表示一个函数将被使用 new 来运行。
function User(name, age = 0) {
this.name = name;
this.age = age;
this.run = function () {
console.log('run');
}
this.walk = () => {
console.log('walk');
}
}
let vipUser = new User('dano', 21);
console.log(vipUser.name);
vipUser.run();
vipUser.walk();
手动实现一个new
在面试题手撸集合中
现代JS教程与阮一峰ES6 · 4原型与类/类
基本语法
在面向对象的编程中,class是用于创建对象的可扩展的程序代码模版,它为对象提供了状态(成员变量)的初始值和行为(成员函数或方法)的实现。
基本语法:
class MyClass {
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
然后使用 new MyClass() 来创建具有上述列出的所有方法的新对象。new 会自动调用 constructor() 方法,因此我们可以在 constructor() 中初始化对象。
class是一个函数,就是constructor构造函数。
例如:
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
// class 是一个函数
alert(typeof User); // function
// ...或者,更确切地说,==是 constructor 方法==
alert(User === User.prototype.constructor); // true
// 方法在 User.prototype 中,例如:
alert(User.prototype.sayHi); // sayHi 方法的代码
// 在原型中实际上有两个方法
alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi

人们常说 class 是一个语法糖(旨在使内容更易阅读,但不引入任何新内容的语法),因为我们实际上可以在不使用 class 的情况下声明相同的内容:
// 用纯函数重写 class User
// 1. 创建构造器函数
function User(name) {
this.name = name;
}
// 函数的原型(prototype)默认具有 "constructor" 属性,
// 所以,我们不需要创建它
// 2. 将方法添加到原型
User.prototype.sayHi = function() {
alert(this.name);
};
// 用法:
let user = new User("John");
user.sayHi();
这个定义的结果与使用类得到的结果基本相同。因此,这确实是将 class 视为一种定义构造器及其原型方法的语法糖的理由。
尽管,它们之间存在着重大差异:
- 首先,通过
class创建的函数==具有特殊的内部属性标记==IsClassConstructor: true。因此,它与手动创建并不完全相同。 - 类方法不可枚举。类定义将
"prototype"中的所有方法的enumerable标志设置为false。这很好,因为如果我们对一个对象调用for..in方法,我们通常不希望 class 方法出现。 - 类总是使用
use strict。在类构造中的所有代码都将自动进入严格模式。
就我的个人理解来说,就是在语法糖的基础上加入了一些新的特性,基本上就差不多
类表达式
就和函数表式一样:
let User = class {
sayHi() {
alert("Hello");
}
};
类似于命名函数表达式(Named Function Expressions),类表达式可能也应该有一个名字。 如果类表达式有名字,那么该名字仅在类内部可见:
// “命名类表达式(Named Class Expression)”
// (规范中没有这样的术语,但是它和命名函数表达式类似)
let User = class MyClass {
sayHi() {
alert(MyClass); // MyClass 这个名字仅在类内部可见
}
};
new User().sayHi(); // 正常运行,显示 MyClass 中定义的内容
alert(MyClass); // error,MyClass 在外部不可见
Getters/setters
就像对象字面量,类可能包括 getters/setters,计算属性(computed properties)等。 这是一个使用 get/set 实现 user.name 的示例:
class User {
constructor(name) {
// 调用 setter
this.name = name;
}
get name() {
return this._name;
}
set name(value) {
if (value.length < 4) {
alert("Name is too short.");
return;
}
this._name = value;
}
}
let user = new User("John");
alert(user.name); // John
user = new User(""); // Name is too short.
从技术上来讲,这样的类声明可以通过在 User.prototype 中创建 getters 和 setters 来实现。
对象中也有类似的访问器属性,类中的getter、setter本质上都是访问器属性(Accessor Property) ,表现形式完全相同。
类 getter/setter 对象访问器属性 位置 在 User.prototype 上 在对象自身上 作用范围 所有实例共享 仅当前对象 ** enumerable** 默认 false 默认 true
从运行结果可以清楚看到:
类属性描述符:
{ get: [Function], set: [Function], enumerable:
false, configurable: true }
对象属性描述符:
{ get: [Function], set: [Function], enumerable:
true, configurable: true }
两者都有 get 和 set ,这证明 它们是完全一样的数据结构 。
图示
类的 getter/setter:
user ──────proto─────> User.prototype
↓
{ get, set } ← 所有实例共用
对象的访问器属性:
obj ──直接拥有──> { _name, get, set } ← 只属于这个对
象
简单记忆:类的 getter/setter 是写在原型上的"实例共享版"访问器属性。
计算属性名称
class User {
['say' + 'Hi']() {
alert("Hello");
}
}
new User().sayHi();
“类字段”
是一种允许添加任何属性的语法。 例如,让我们在 class User 中添加一个 name 属性:
class User {
name = "John";
sayHi() {
alert(`Hello, ${this.name}!`);
}
}
new User().sayHi(); // Hello, John!
类字段的重要区别在于,它们会被挂在实例对象上,而非 User.prototype 上 
正如 函数绑定 一章中所讲的,JavaScript 中的函数具有动态的 this。它取决于调用上下文。 因此,如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则 this 将不再是对其对象的引用。 类字段提供了一种很优雅的写法:
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // hello
class Button {
constructor(value) {
this.value = value;
}
click() {
console.log(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // undefined
setTimeout(() => button.click(), 1000);//hello
类继承
继承另一个类的语法是:class Child extends Parent  在内部,关键字 extends 使用了很好的旧的原型机制进行工作。它==将 Rabbit.prototype.Prototype(隐式原型) 设置为 Animal.prototype==。所以,如果在 Rabbit.prototype 中找不到一个方法,JavaScript 就会从 Animal.prototype 中获取该方法。
类语法不仅允许指定一个类,在 extends 后可以指定任意表达式。
重写方法
"super" 关键字。
- 执行
super.method(...)来调用一个父类方法。 - 执行
super(...)来调用一个父类 constructor(只能在我们的 constructor 中)。
class Animal {
constructor(name) {
this.name = name;
}
run() {
console.log('animal run');
}
}
class Rabbit extends Animal {
run() {
super.run();//animal run
console.log('rabbit run');//rabbit run
}
}
let rabbit = new Rabbit("White Rabbit");
rabbit.run();
箭头函数没有 super。如果被访问,它会从外部函数获取。
如果一个类扩展了另一个类并且没有 constructor,那么将生成下面这样的“空” constructor:
class Rabbit extends Animal {
// 为没有自己的 constructor 的扩展类生成的
constructor(...args) {
super(...args);
}
}
继承类的 constructor 必须调用 super(...),并且 一定要在使用 this 之前调用。
在 JavaScript 中,继承类(所谓的“派生构造器”,英文为 “derived constructor”)的构造函数与其他函数之间是有区别的。派生构造器具有特殊的内部属性 ConstructorKind:"derived"。这是一个特殊的内部标签。
该标签会影响它的 new 行为:
- 当通过
new执行一个常规函数时,它将创建一个空对象,并将这个空对象赋值给this。 - 但是当继承的 constructor 执行时,它不会执行此操作。它期望父类的 constructor 来完成这项工作。
因此,派生的 constructor 必须调用 super 才能执行其父类(base)的 constructor,否则 this 指向的那个对象将不会被创建。并且我们会收到一个报错。
为了让 Rabbit 的 constructor 可以工作,它需要在使用 this 之前调用 super(),就像下面这样:重写constructor
class Animal {
constructor(name) {
this.name = name;
}
run() {
console.log('animal run');
}
}
class Rabbit extends Animal {
constructor(name,age) {
super(name);
this.age =age;
}
run() {
super.run();//animal run
console.log('rabbit run');//rabbit run
}
}
let rabbit = new Rabbit("White Rabbit",21);
console.log(rabbit);//Rabbit { name: 'White Rabbit', age: 21 }
我们不仅可以重写方法,还可以重写类字段。 不过,当我们在父类构造器中访问一个被重写的字段时,有一个诡异的行为,这与绝大多数其他编程语言都很不一样。
当父类构造器在派生的类中被调用时,它会使用被重写的方法。但是父类构造器总是会使用它自己字段的值,而不是被重写的那一个。
没看完。4/8/26 快要byteintern面试了,复习一波
静态属性和静态方法
它们以 static 关键字开头,静态方法可以在类上调用,而不是在单个对象上。
class User {
static staticMethod() {
alert(this === User);
}
}
User.staticMethod(); // true
他们本质是一样的
class User { }
User.staticMethod = function() {
alert(this === User);
};
User.staticMethod(); // true
它不是对象的方法,而是整个 class 的方法。 静态的属性是一样的。静态属性和方法是可被继承的。
受保护的、私有的属性和方法
受保护的属性通常以下划线 _ 作为前缀。
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(value) {
if (value < 0) {
value = 0;
}
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
}
// 创建咖啡机
let coffeeMachine = new CoffeeMachine(100);
// 加水
coffeeMachine.waterAmount = -10; // _waterAmount 将变为 0,而不是 -10
如果属性不想被修改,不设置setter即可
推荐使用者种:
class CoffeeMachine {
_waterAmount = 0;
setWaterAmount(value) {
if (value < 0) value = 0;
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
受保护的字段是可以继承的; 如果我们继承 class MegaMachine extends CoffeeMachine,那么什么都无法阻止我们从新的类中的方法访问 this._waterAmount 或 this._power。
私有的属性
这是最近加入的新特性。 私有属性和方法应该以 # 开头。它们只在类的内部可被访问。 在语言级别,# 是该字段为私有的特殊标志。我们无法从外部或从继承的类中访问它。
私有字段与公共字段不会发生冲突。我们可以同时拥有私有的 #waterAmount 和公共的 waterAmount 字段。
如果我们继承自 CoffeeMachine,那么我们将无法直接访问 #waterAmount。我们需要依靠 waterAmount getter/setter
扩展内建类(使用内建类做父类)
内建类却是一个例外。它们相互间不继承静态方法。
类检查
| 用于 | 返回值 |
|---|---|
typeof | 原始数据类型 |
{}.toString | 原始数据类型,内建对象,包含 Symbol.toStringTag 属性的对象 |
instanceof | 对象 |
正如我们所看到的,从技术上讲,{}.toString 是一种“更高级的” typeof。 |
当我们使用类的层次结构(hierarchy),并想要对该类进行检查,同时还要考虑继承时,这种场景下 instanceof 操作符确实很出色。
Mixin模式
没看,
Mixin(混入)模式是一种在 JavaScript 中实现代码复用和模拟多继承的流行技术。它的核心思想是:将一个或多个对象(称为 Mixin)的方法或属性,“复制”到另一个类或对象的原型上,从而为目标对象增加新的功能,而无需使用传统的继承。
简单来说,Mixin 模式是一种“组合优于继承”的设计思想,它允许你像搭积木一样,将不同的功能模块组合到一个类中。
🤔 为什么需要 Mixin 模式?
JavaScript 的类(class)只支持单继承,即一个类只能 extends 一个父类。但在实际开发中,我们常常需要一个类具备多种独立的能力。例如,一个 User 类可能同时需要“可序列化”和“可事件化”的能力。
如果只用继承,我们可能需要创建复杂的继承链,这会导致代码耦合度高、难以维护。Mixin 模式提供了一种更灵活的横向扩展方式,让一个类轻松拥有多种能力。
🛠️ 如何实现 Mixin 模式?
在 JavaScript 中,主要有两种常见的实现方式:
使用 Object.assign() 进行属性复制
这是最直接、最常见的方法。通过 Object.assign() 将 Mixin 对象的属性浅拷贝到目标类的原型(prototype)上。
// 定义一个可飞行的 Mixin
const Flyable = {
fly() {
console.log('I can fly!');
}
};
// 定义一个可游泳的 Mixin
const Swimmable = {
swim() {
console.log('I can swim!');
}
};
// 定义一个基类
class Animal {}
// 将 Mixin 的功能混入到 Animal 类的原型中
Object.assign(Animal.prototype, Flyable, Swimmable);
// 创建实例并测试
const duck = new Animal();
duck.fly(); // 输出: I can fly!
duck.swim(); // 输出: I can swim!
优点:简单直观,易于理解和实现。缺点:
- 方法冲突:如果多个 Mixin 包含同名方法,后混入的会覆盖先混入的,可能引发意外行为。
- 浅拷贝:
Object.assign()是浅拷贝,如果 Mixin 的属性是对象,拷贝的只是引用,修改原始 Mixin 会影响目标对象。 - 不支持
super:无法通过super关键字调用原型链上的同名方法。
使用高阶函数(类工厂)
这是一种更现代、更强大的方式。通过定义一个函数,该函数接收一个基类作为参数,并返回一个扩展后的新类。
// 定义一个返回新类的高阶函数 Mixin
const withFlying = (BaseClass) => {
return class extends BaseClass {
fly() {
// 如果父类也有 fly 方法,可以通过 super 调用
if (super.fly) super.fly();
console.log('Flying with mixin!');
}
};
};
const withSwimming = (BaseClass) => {
return class extends BaseClass {
swim() {
console.log('Swimming with mixin!');
}
};
};
// 定义一个基类
class Bird {}
// 通过嵌套调用 Mixin 函数来组合功能
class Duck extends withSwimming(withFlying(Bird)) {}
// 创建实例并测试
const donaldDuck = new Duck();
donaldDuck.fly(); // 输出: Flying with mixin!
donaldDuck.swim(); // 输出: Swimming with mixin!
优点:
- 支持
super:可以正确地调用原型链上的方法,解决了方法冲突问题。 - 组合性强:可以清晰地链式组合多个 Mixin。
- 不污染原型:不会直接修改原始基类的原型。
缺点:写法相对复杂一些。
✨ Mixin 模式的优缺点
优点
- 代码复用:将通用功能(如日志、事件处理、缓存等)抽离成独立的 Mixin,可以在多个类之间轻松共享,避免代码重复。
- 灵活性高:可以按需组合功能,构建出行为各异的类,比僵化的继承层次结构更灵活。
- 模拟多继承:在 JavaScript 单继承的限制下,提供了一种实现多继承效果的优雅方案。
缺点
- 方法名冲突:当混入的多个对象包含同名方法时,需要手动处理冲突,否则会导致方法被覆盖。
- 来源不明确:过度使用 Mixin 可能导致一个类的方法来自多个地方,增加了代码的追踪和维护难度。
- 原型污染风险:直接修改原型(如使用
Object.assign)可能会带来意想不到的副作用。
🌐 实际应用:Vue.js 中的 Mixin
Vue.js 框架就广泛使用了 Mixin 的思想。你可以将组件中可复用的选项(如 data, methods, created 钩子等)定义在一个 Mixin 对象中,然后在多个组件中通过 mixins 选项引入。
// 定义一个 Mixin 对象
const myMixin = {
data() {
return {
message: 'Hello from Mixin!'
}
},
created() {
console.log('Mixin created hook called.');
}
};
// 在 Vue 组件中使用
const MyComponent = {
mixins: [myMixin],
data() {
return {
// 组件自己的数据
}
}
// 当组件被创建时,会先执行 Mixin 中的 created 钩子
};
当 Mixin 和组件本身存在同名选项时,Vue 有一套明确的合并策略(例如,对于 data,组件的数据会优先于 Mixin 的数据),这解决了大部分冲突问题。
现代JS教程与阮一峰ES6 · 4原型与类/原型
直接看原型链吧
在编程中,我们经常会想获取并扩展一些东西。
例如,我们有一个 user 对象及其属性和方法,并希望将 admin 和 guest 作为基于 user 稍加修改的变体。我们想重用 user 中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象。
原型继承(Prototypal inheritance) 这个语言特性能够帮助我们实现这一需求。
在 JavaScript 中,对象有一个特殊的隐藏属性 Prototype(原型)(如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”。  当我们从 object 中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。在编程中,这被称为“原型继承”。
属性 Prototype 是内部的而且是隐藏的,但是这儿有很多设置它的方式。
使用特殊的名字 __proto__:
let animal = {
eats: true,
}
let rabbit = {
jumps: true,
}
rabbit.__proto__ = animal;
console.log(rabbit.eats);//true
rabbit.eat();//eating!!!
如果 animal 有许多有用的属性和方法,那么它们将自动地变为在 rabbit 中可用。这种属性被称为“继承”。
原型链可以很长,但是有两个限制:
- 不能形成闭环
——proto——的值==只能是对象或者null==,其他的值将会自动的被忽略
只能有一个 Prototype。一个对象不能从其他两个对象获得继承。
__proto__ 是 Prototype 的因历史原因而留下来的 getter/setter 初学者常犯一个普遍的错误,就是不知道 __proto__ 和 Prototype 的区别。 请注意,__proto__ 与内部的 Prototype 不一样。__proto__ 是 Prototype 的 getter/setter。
__proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 ==Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__== 去 get/set 原型。
在写入和删除的时候,使用的是直接的对象,而不是原型。读取时是原型:
let animal = {
eats: true,
walk() {
console.log('animal walk!!!');
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk();//animal walk!!!
rabbit.walk = function () {
console.log("Rabbit! Bounce-bounce!");
};
rabbit.walk(); // Rabbit! Bounce-bounce!
animal.walk();//animal walk!!!

访问器(accessor)属性是一个例外,因为赋值(assignment)操作是由 setter 函数处理的。因此,写入此类属性实际上与调用函数相同。
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
console.log(admin.fullName); // John Smith (*)
admin.name = 'Dano';
console.log(admin.fullName);//Dano Smith
console.log(user.fullName);//John Smith
console.log(user.name);//John
admin.fullName = "Alice Cooper"; //(**)
console.log(admin.fullName); // Alice Cooper,admin 的内容被修改了
console.log(user.fullName); // John Smith,user 的内容被保护了
在 (*) 行中,属性 admin.fullName 在原型 user 中有一个 getter,因此它会被调用。在 (**) 行中,属性在原型中有一个 setter,因此它会被调用。
在上面的例子中可能会出现一个有趣的问题:在 set fullName(value) 中 this 的值是什么?属性 this.name 和 this.surname 被写在哪里:在 user 还是 admin?
答案很简单:this 根本不受原型的影响。 无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象。 因此,setter 调用 admin.fullName= 使用 admin 作为 this,而不是 user。
也就是说,普通的属性写入是在对象上进行的。对于访问器属性(虚假的属性),他是调用的原型的方法来实现的,方法中的this却是对象的而不是原型的,那么设置的也就是对象的属性。
所以,方法是共享的,但对象状态不是。
Object.keys(obj)只会返回自己的属性,而不会返回原型的属性 而for...in会返回原型和对象的属性。使用内建方法obj.hasOwnProperty(key)如果属性是自己的就返回true,是原型的就返回false。
let user = {
name: "John",
surname: "Smith",
set fullName(value) {
[this.name, this.surname] = value.split(" ");
},
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
console.log(Object.keys(admin));//[ 'isAdmin' ]
for (let prop in admin) console.log(prop);
/**
isAdmin
name
surname
fullName
*/
for (let prop in admin) {
if (admin.hasOwnProperty(prop)) {
console.log('对象的属性:' + prop);
} else {
console.log('原型的属性:' + prop);
}
};
/**
* 对象的属性:isAdmin
原型的属性:name
原型的属性:surname
原型的属性:fullName
*/
这里我们有以下继承链:rabbit 从 animal 中继承,animal 从 Object.prototype 中继承(因为 animal 是对象字面量 {...},所以这是默认的继承),然后再向上是 null  这有一件很有趣的事儿。方法 rabbit.hasOwnProperty 来自哪儿?我们并没有定义它。从上图中的原型链我们可以看到,该方法是 Object.prototype.hasOwnProperty 提供的。换句话说,它是继承的。
……如果 for..in 循环会列出继承的属性,那为什么 hasOwnProperty 没有像 eats 和 jumps 那样出现在 for..in 循环中?
答案很简单:它是不可枚举的。就像 Object.prototype 的其他属性,hasOwnProperty 有 enumerable:false 标志。并且 for..in 只会列出可枚举的属性。这就是为什么它和其余的 Object.prototype 属性都未被列出。
在现代引擎中,从性能的角度来看,我们是从对象还是从原型链获取属性都是没区别的。它们(引擎)会记住在哪里找到的该属性,并在下一次请求中重用它。
构造函数的Prototype
这里的 F.prototype 指的是 F 的一个名为 "prototype" 的常规属性。这听起来与“原型”这个术语很类似,但这里我们实际上指的是具有该名字的常规属性。
==设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 Prototype 赋值为 animal”。==
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = animal;
let rabbit = new Rabbit("White Rabbit"); // rabbit.__proto__ == animal
console.log(rabbit.eats);// true
function Rabbit2(name) {
this.name = name;
this.__proto__ = animal;
}
let rabbit2 = new Rabbit2('Dano');
console.log(rabbit2.eats);//true
也就是说,F.prototype=obj就是在F(){this.proto=obj}的效果是一样的。  ==F.prototype 属性仅在 new F 被调用时使用,它为新对象的 Prototype 赋值。==
如果在创建之后,F.prototype 属性有了变化(F.prototype = <another object>),那么通过 new F 创建的新对象也将随之拥有新的对象作为 Prototype,但已经存在的对象将保持旧有的值。就是不会动态的改变,这符合常识。
每个函数都有 "prototype" 属性,即使我们没有提供它。 默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。  ……JavaScript 自身并不能确保正确的 "constructor" 函数值。
是的,它存在于函数的默认 "prototype" 中,但仅此而已。之后会发生什么 —— 完全取决于我们。 特别是,如果我们将整个默认 prototype 替换掉,那么其中就不会有 "constructor" 了。
就是说,每个函数都会有一个默认的prototype属性,这个属性的值是个对象(object),这个对象里面有一个constructor属性,值为构造函数F.prototype = {constructor: F}
通过这一点我们可以使用现在已经有的对象来构造一个对象:
function Rabbit(name) {
this.name = name;
alert(name);
}
let rabbit = new Rabbit("White Rabbit");
let rabbit2 = new rabbit.constructor("Black Rabbit");

一切都很简单,只需要记住几条重点就可以清晰地掌握了:
F.prototype属性(不要把它与Prototype弄混了)在new F被调用时为新对象的Prototype赋值。F.prototype的值要么是一个对象,要么就是null:其他值都不起作用。"prototype"属性仅当设置在一个构造函数上,并通过new调用时,才具有这种特殊的影响。
在常规对象上,prototype 没什么特别的:
let user = {
name: "John",
prototype: "Bla-bla" // 这里只是普通的属性
};
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
alert( rabbit.eats ); // true
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
Rabbit.prototype = {};
alert( rabbit.eats ); // ?
答案是true,应为一但rabbit构造出来它的prototype的引用所指向的对象就不会变了,而对象本身还是可以改变的这和const声明一个对象很像:
function Rabbit() {}
Rabbit.prototype = {
eats: true
};
let rabbit = new Rabbit();
Rabbit.prototype.eats = false;
alert( rabbit.eats ); // ?
在这里,直接修改的对象里面的属性的值,而不是修改整个对象,就会成功。
原生的原型
let obj = {};
alert( obj ); //[object Object]
简短的表达式 obj = {} 和 obj = new Object() 是一个意思,其中 Object 就是一个内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。  当 new Object() 被调用(或一个字面量对象 {...} 被创建),按照前面章节中我们学习过的规则,这个对象的 Prototype 属性被设置为 Object.prototype
就是说,Object()这个函数的prototype属性指向一个巨大的对象,这和之前的:
let human = {
age: 21,
}
function Users(name) {
this.name = name;
console.log(`obj ${name} created!!!`);
}
Users.prototype = human;
let users = new Users('dano');
console.log(users.__proto__);
的形式很相似。
也就是说,这个Object构造函数的prototype被那个巨大的对象赋值
let obj = {};
alert(obj.__proto__ === Object.prototype); // true
alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true
其他内建对象,像 Array、Date、Function 及其他,都在 prototype 上挂载了方法。
例如,当我们创建一个数组 [1, 2, 3],在内部会默认使用 new Array() 构造器。因此 Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。
所有的内建原型顶端都是 Object.prototype。这就是为什么有人说“一切都从对象继承而来”。  一些方法在原型上可能会发生重叠,例如,Array.prototype 有自己的 toString 方法来列举出来数组的所有元素并用逗号分隔每一个元素。正如我们之前看到的那样,Object.prototype 也有 toString 方法,但是 Array.prototype 在原型链上更近,所以数组对象原型上的方法会被使用。 
基本数据类型,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 String、Number 和 Boolean 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。
这些对象对我们来说是无形地创建出来的。大多数引擎都会对其进行优化,但是规范中描述的就是通过这种方式。这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototype、Number.prototype 和 Boolean.prototype 进行获取。
特殊值 null 和 undefined 比较特殊。它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型。
原生的原型是可以被修改的。例如,我们向 String.prototype 中添加一个方法,这个方法将对所有的字符串都是可用的。在开发的过程中,我们可能会想要一些新的内建方法,并且想把它们添加到原生原型中。但这通常是一个很不好的想法。
在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfilling(垫片)。
Polyfilling 是一个术语,表示某个方法在 JavaScript 规范中已存在,但是特定的 JavaScript 引擎尚不支持该方法,那么我们可以通过手动实现它,并用以填充内建原型。
从原型中借用函数:
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
console.log(obj.join(',')); // Hello,world!
上面这段代码有效,是因为内建的方法 join 的内部算法只关心正确的索引和 length 属性。它不会检查这个对象是否是真正的数组。许多内建方法就是这样。
另一种方式是通过将 obj.__proto__ 设置为 Array.prototype,这样 Array 中的所有方法都自动地可以在 obj 中使用了。
但是如果 obj 已经从另一个对象进行了继承,那么这种方法就不可行了(译注:因为这样会覆盖掉已有的继承。此处 obj 其实已经从 Object 进行了继承,但是 Array 也继承自 Object,所以此处的方法借用不会影响 obj 对原有继承的继承,因为 obj 通过原型链依旧继承了 Object)。请记住,我们一次只能继承一个对象。
原型方法和没有__proto__的对象
使用 obj.__proto__ 设置或读取原型被认为已经过时且不推荐使用(deprecated)了(已经被移至 JavaScript 规范的附录 B,意味着仅适用于浏览器)。
现代的获取/设置原型的方法有:
- Object.getPrototypeOf(obj) —— 返回对象 obj 的 Prototype。
- Object.setPrototypeOf(obj, proto) —— 将对象 obj 的 Prototype 设置为 proto。
__proto__ 不被反对的唯一的用法是在创建新对象时,将其用作属性:{ __proto__: ... }
Object.create(proto, [descriptors]) —— 利用给定的 proto 作为 Prototype 和可选的属性描述来创建一个空对象。
let animal = {
eats: true,
};
let rabbit = Object.create(animal);
console.log(rabbit.eats); //true
console.log(Object.getPrototypeOf(rabbit) === animal);//true
Object.setPrototypeOf(rabbit, {});
console.log(rabbit.eats);//undefined
Object.create 方法更强大,因为它有一个可选的第二参数:属性描述器 描述器的格式与 属性标志和属性描述符 一章中所讲的一样:
let animal = {
eats: true,
};
let rabbit = Object.create(animal, {
likes: {
value:true,
}
});
console.log(rabbit.likes);//true
console.log(Object.getOwnPropertyDescriptors(rabbit));
/**
* {
likes: {
value: true,
writable: false,
enumerable: false,
configurable: false
}
}
*/
console.log(rabbit.eats);//true
我们可以使用 Object.create 来实现比复制 for..in 循环中的属性更强大的对象克隆方式:
let clone = Object.create(
Object.getPrototypeOf(obj),//原型
Object.getOwnPropertyDescriptors(obj)//属性描述
);
是浅拷贝。
从技术上来讲,我们可以在任何时候 get/set Prototype。但是通常我们只在创建对象的时候设置它一次,自那之后不再修改:rabbit 继承自 animal,之后不再更改。
并且,JavaScript 引擎对此进行了高度优化。用 Object.setPrototypeOf 或 obj.__proto__= “即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化。因此,除非你知道自己在做什么,或者 JavaScript 的执行速度对你来说完全不重要,否则请避免使用它。
let obj = {};
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // [object Object],并不是 "some value"!
这里如果用户输入 __proto__,那么在第四行的赋值会被忽略!
对于非开发者来说,这肯定很令人惊讶,但对我们来说却是可以理解的。__proto__ 属性很特殊:它必须是一个对象或者 null。字符串不能成为原型。这就是为什么将字符串赋值给 __proto__ 会被忽略。
通常开发者完全不会考虑到这一点。这让此类 bug 很难被发现,甚至变成漏洞,尤其是在 JavaScript 被用在服务端的时候。
现在,我们想要将一个对象用作关联数组,并且摆脱此类问题,我们可以使用一些小技巧:
let obj = Object.create(null);
// 或者:obj = { __proto__: null }
let key = prompt("What's the key?", "__proto__");
obj[key] = "some value";
alert(obj[key]); // "some value"
Object.create(null) 创建了一个空对象,这个对象没有原型(Prototype 是 null)  因此,它没有继承 __proto__ 的 getter/setter 方法。现在,它被作为正常的数据属性进行处理,因此上面的这个示例能够正常工作。
我们可以把这样的对象称为 “very plain” 或 “pure dictionary” 对象,因为它们甚至比通常的普通对象(plain object){...} 还要简单。
大多数与对象相关的方法都是 Object.something(...),例如 Object.keys(obj) —— 它们不在 prototype 中,因此在 “very plain” 对象中它们还是可以继续使用:
let chineseDictionary = Object.create(null);
chineseDictionary.hello = "你好";
chineseDictionary.bye = "再见";
alert(Object.keys(chineseDictionary)); // hello,bye
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype.sayHi = function () {
console.log(this.name);
};
let rabbit = new Rabbit("Rabbit");
rabbit.sayHi();//Rabbit
Rabbit.prototype.sayHi();//undefined
Object.getPrototypeOf(rabbit).sayHi();//undefined
rabbit.__proto__.sayHi();//undefined
除了第一个的this是rabbit,剩下的都是Rabbit.prototype,所以都是undefined
现代JS教程与阮一峰ES6 · 4原型与类/原型链
原型链
原型链的全貌图 
原型链的理解很简单,背这个图没有意义:
- 一个对象有__proto__属性,隐式原型
- 一个函数有prototype属性,原型属性
- 一个对象由new调用其构造函数而来
- 构造函数也是对象
画图的技巧: 一个对象的隐式原型和将他new出来的函数的原型属性是一样的
这里有两个特例:
Object.prototype.__proto__是nullFunction.prototype===Function.__proto__
其余的按这个规律就可以全部画出
原型图中的关系均是正确的:
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
const p1 = new Person('dano', 21)
console.log(p1.__proto__)
// {}
console.log(Person.prototype)
// {}
console.log(p1.__proto__ === Person.prototype)
// true
console.log(Object.prototype.__proto__)
// null
console.log(Object.__proto__ === Function.prototype)
// true
console.log(Function.prototype)
// [Function (anonymous)] Object
console.log(Function.prototype===Function.__proto__)
// true
console.log(Function.prototype.__proto__.__proto__)
// null
console.log(Function.prototype.__proto__===Person.prototype.__proto__)
// true
廖雪峰教程 · 6对象/6.0 简述
JavaScript的所有数据都可以看成对象,那是不是我们已经在使用面向对象编程了呢?
当然不是。如果我们只使用Number、Array、string以及基本的{...}定义的对象,还无法发挥出面向对象编程的威力。
JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。如果你熟悉Java或C#,很好,你一定明白面向对象的两个基本概念:
- 类:类是对象的类型模板,例如,定义
Student类来表示学生,类本身是一种类型,Student表示学生类型,但不表示任何具体的某个学生; - 实例:实例是根据类创建的对象,例如,根据
Student类可以创建出xiaoming、xiaohong、xiaojun等多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。
所以,类和实例是大多数面向对象编程语言的基本概念。
不过,在JavaScript中,这个概念需要改一改。JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。
原型是指当我们想要创建xiaoming这个具体的学生时,我们并没有一个Student类型可用。那怎么办?恰好有这么一个现成的对象:
let robot = {
name: 'Robot',
height: 1.6,
run: function () {
console.log(this.name + ' is running...');
}
};
我们看这个robot对象有名字,有身高,还会跑,有点像小明,干脆就根据它来“创建”小明得了!
于是我们把它改名为Student,然后创建出xiaoming:
let Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
let xiaoming = {
name: '小明'
};
xiaoming.__proto__ = Student;
注意最后一行代码把xiaoming的原型指向了对象Student,看上去xiaoming仿佛是从Student继承下来的:
xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running...
xiaoming有自己的name属性,但并没有定义run()方法。不过,由于小明是从Student继承而来,只要Student有run()方法,xiaoming也可以调用: 
JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。
如果你把xiaoming的原型指向其他对象:
let Bird = {
fly: function () {
console.log(this.name + ' is flying...');
}
};
xiaoming.__proto__ = Bird;
现在xiaoming已经无法run()了,他已经变成了一只鸟:
xiaoming.fly(); // 小明 is flying...
在JavaScrip代码运行时期,你可以把xiaoming从Student变成Bird,或者变成任何对象。
请注意,上述代码仅用于演示目的。在编写JavaScript代码时,不要直接用obj.__proto__去改变一个对象的原型,并且,低版本的IE也无法使用__proto__。Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有,因此,我们可以编写一个函数来创建xiaoming:
// 原型对象:
let Student = {
name: 'Robot',
height: 1.2,
run: function () {
console.log(this.name + ' is running...');
}
};
function createStudent(name) {
// 基于Student原型创建一个新对象:
let s = Object.create(Student);
// 初始化新对象:
s.name = name;
return s;
}
let xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true
这是创建原型继承的一种方法,JavaScript还有其他方法来创建对象,我们在后面会一一讲到。
廖雪峰教程 · 6对象/6.1 创建对象
创建对象
JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。
当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined。
例如,创建一个Array对象:
let arr = [1, 2, 3];
其原型链是:
null
▲
│
┌─────────────────┐
│Object.prototype │
└─────────────────┘
▲
│
┌─────────────────┐
│ Array.prototype │
└─────────────────┘
▲
│
┌─────────────────┐
│ arr │
└─────────────────┘
Array.prototype定义了indexOf()、shift()等方法,因此你可以在所有的Array对象上直接调用这些方法。
当我们创建一个函数时:
function foo() {
return 0;
}
函数也是一个对象,它的原型链是:
null
▲
│
┌───────────────────┐
│ Object.prototype │
└───────────────────┘
▲
│
┌───────────────────┐
│Function.prototype │
└───────────────────┘
▲
│
┌───────────────────┐
│ foo │
└───────────────────┘
由于Function.prototype定义了apply()等方法,因此,所有函数都可以调用apply()方法。
很容易想到,如果原型链很长,那么访问一个对象的属性就会因为花更多的时间查找而变得更慢,因此要注意不要把原型链搞得太长。
构造函数
除了直接用{ ... }创建一个对象外,JavaScript还可以用一种构造函数的方法来创建对象。它的用法是,先定义一个构造函数:
function Student(name) {
this.name = name;
this.hello = function () {
alert('Hello, ' + this.name + '!');
}
}
你会问,咦,这不是一个普通函数吗?
这确实是一个普通函数,但是在JavaScript中,可以用关键字new来调用这个函数,并返回一个对象:
let xiaoming = new Student('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!
注意,如果不写new,这就是一个普通函数,它返回undefined。但是,如果写了new,它就变成了一个构造函数,它绑定的this指向新创建的对象,并默认返回this,也就是说,不需要在最后写return this;。
新创建的xiaoming的原型链是:
null
▲
│
┌─────────────────┐
│Object.prototype │
└─────────────────┘
▲
│
┌─────────────────┐
│Student.prototype│
└─────────────────┘
▲
│
┌─────────────────┐
│ xiaoming │
└─────────────────┘
也就是说,xiaoming的原型指向函数Student的原型。如果你又创建了xiaohong、xiaojun,那么这些对象的原型与xiaoming是一样的:
null
▲
│
┌─────────────────┐
│Object.prototype │
└─────────────────┘
▲
│
┌─────────────────┐
│Student.prototype│
└─────────────────┘
▲ ▲ ▲
│ │ │
┌─────────┐┌─────────┐┌─────────┐
│xiaoming ││xiaohong ││ xiaojun │
└─────────┘└─────────┘└─────────┘
用new Student()创建的对象还从原型上获得了一个constructor属性,它指向函数Student本身:
xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true
Object.getPrototypeOf(xiaoming) === Student.prototype; // true
xiaoming instanceof Student; // true
看晕了吧?用一张图来表示这些乱七八糟的关系就是: 
红色箭头是原型链。注意,Student.prototype指向的对象就是xiaoming、xiaohong的原型对象,这个原型对象自己还有个属性constructor,指向Student函数本身。
另外,函数Student恰好有个属性prototype指向xiaoming、xiaohong的原型对象,但是xiaoming、xiaohong这些对象可没有prototype这个属性,不过可以用__proto__这个非标准用法来查看。
现在我们就认为xiaoming、xiaohong这些对象“继承”自Student。
不过还有一个小问题,注意观察:
xiaoming.name; // '小明'
xiaohong.name; // '小红'
xiaoming.hello; // function: Student.hello()
xiaohong.hello; // function: Student.hello()
xiaoming.hello === xiaohong.hello; // false
xiaoming和xiaohong各自的name不同,这是对的,否则我们无法区分谁是谁了。
xiaoming和xiaohong各自的hello是一个函数,但它们是两个不同的函数,虽然函数名称和代码都是相同的!
如果我们通过new Student()创建了很多对象,这些对象的hello函数实际上只需要共享同一个函数就可以了,这样可以节省很多内存。
要让创建的对象共享一个hello函数,根据对象的属性查找原则,我们只要把hello函数移动到xiaoming、xiaohong这些对象共同的原型上就可以了,也就是Student.prototype: 
修改代码如下:
function Student(name) {
this.name = name;
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
};
用new创建基于原型的JavaScript的对象就是这么简单!
忘记写new怎么办
如果一个函数被定义为用于创建对象的构造函数,但是调用时忘记了写new怎么办?
在strict模式下,this.name = name将报错,因为this绑定为undefined,在非strict模式下,this.name = name不报错,因为this绑定为window,于是无意间创建了全局变量name,并且返回undefined,这个结果更糟糕。
所以,调用构造函数千万不要忘记写new。为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写,这样,一些语法检查工具如jslint将可以帮你检测到漏写的new。
最后,我们还可以编写一个createStudent()函数,在内部封装所有的new操作。一个常用的编程模式像这样:
function Student(props) {
this.name = props.name || '匿名'; // 默认值为'匿名'
this.grade = props.grade || 1; // 默认值为1
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
};
function createStudent(props) {
return new Student(props || {})
}
这个createStudent()函数有几个巨大的优点:一是不需要new来调用,二是参数非常灵活,可以不传,也可以这么传:
let xiaoming = createStudent({
name: '小明'
});
xiaoming.grade; // 1
如果创建的对象有很多属性,我们只需要传递需要的某些属性,剩下的属性可以用默认值。由于参数是一个Object,我们无需记忆参数的顺序。如果恰好从JSON拿到了一个对象,就可以直接创建出xiaoming。
廖雪峰教程 · 6对象/6.2 原型继承
原型继承
在传统的基于Class的语言如Java、C++中,继承的本质是扩展一个已有的Class,并生成新的Subclass。
由于这类语言严格区分类和实例,继承实际上是类型的扩展。但是,JavaScript由于采用原型继承,我们无法直接扩展一个Class,因为根本不存在Class这种类型。
但是办法还是有的。我们先回顾Student构造函数:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
以及Student的原型链: 
现在,我们要基于Student扩展出PrimaryStudent,可以先定义出PrimaryStudent:
function PrimaryStudent(props) {
// 调用Student构造函数,绑定this变量:
Student.call(this, props);
this.grade = props.grade || 1;
}
但是,调用了Student构造函数不等于继承了Student,PrimaryStudent创建的对象的原型是:
new PrimaryStudent() --> PrimaryStudent.prototype --> Object.prototype --> null
必须想办法把原型链修改为:
new PrimaryStudent() --> PrimaryStudent.prototype --> Student.prototype --> Object.prototype --> null
这样,原型链对了,继承关系就对了。新的基于PrimaryStudent创建的对象不但能调用PrimaryStudent.prototype定义的方法,也可以调用Student.prototype定义的方法。
如果你想用最简单粗暴的方法这么干:
PrimaryStudent.prototype = Student.prototype;
是不行的!如果这样的话,PrimaryStudent和Student共享一个原型对象,那还要定义PrimaryStudent干啥?
我们必须借助一个中间对象来实现正确的原型链,这个中间对象的原型要指向Student.prototype。为了实现这一点,参考道爷(就是发明JSON的那个道格拉斯)的代码,中间对象可以用一个空函数F来实现:
// PrimaryStudent构造函数:
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 空函数F:
function F() {
}
// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;
// 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();
// 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;
// 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
// 创建xiaoming:
let xiaoming = new PrimaryStudent({
name: '小明',
grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2
// 验证原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true
// 验证继承关系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true
用一张图来表示新的原型链: 
注意,函数F仅用于桥接,我们仅创建了一个new F()实例,而且,没有改变原有的Student定义的原型链。
如果把继承这个动作用一个inherits()函数封装起来,还可以隐藏F的定义,并简化代码:
function inherits(Child, Parent) {
let F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
这个inherits()函数可以复用:
function Student(props) {
this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
function PrimaryStudent(props) {
Student.call(this, props);
this.grade = props.grade || 1;
}
// 实现原型继承链:
inherits(PrimaryStudent, Student);
// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
return this.grade;
};
廖雪峰教程 · 6对象/6.3 类的继承
类的继承
在上面的章节中我们看到了JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。
有没有更简单的写法?有!
新的关键字class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单。
我们先回顾用函数实现Student的方法:
function Student(name) {
this.name = name;
}
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!');
}
如果用新的class关键字来编写Student,可以这样写:
class Student {
constructor(name) {
this.name = name;
}
hello() {
alert('Hello, ' + this.name + '!');
}
}
比较一下就可以发现,class的定义包含了构造函数constructor和定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了Student.prototype.hello = function () {...}这样分散的代码。
最后,创建一个Student对象代码和前面章节完全一样:
let xiaoming = new Student('小明');
xiaoming.hello();
class继承
用class定义对象的另一个巨大的好处是继承更方便了。想一想我们从Student派生一个PrimaryStudent需要编写的代码量。现在,原型继承的中间对象,原型对象的构造函数等等都不需要考虑了,直接通过extends来实现:
class PrimaryStudent extends Student {
constructor(name, grade) {
super(name); // 记得用super调用父类的构造方法!
this.grade = grade;
}
myGrade() {
alert('I am at grade ' + this.grade);
}
}
注意PrimaryStudent的定义也是class关键字实现的,而extends则表示原型链对象来自Student。子类的构造函数可能会与父类不太相同,例如,PrimaryStudent需要name和grade两个参数,并且需要通过super(name)来调用父类的构造函数,否则父类的name属性无法正常初始化。
PrimaryStudent已经自动获得了父类Student的hello方法,我们又在子类中定义了新的myGrade方法。
ES6引入的class和原有的JavaScript原型继承有什么区别呢?实际上它们没有任何区别,class的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码。